Skip to content

feat(grants): add DELETE /v1/grants/{grantId} for owner-driven revocation - #80

Open
tnunamak wants to merge 1 commit into
mainfrom
tim/account-app-owner-auth
Open

feat(grants): add DELETE /v1/grants/{grantId} for owner-driven revocation#80
tnunamak wants to merge 1 commit into
mainfrom
tim/account-app-owner-auth

Conversation

@tnunamak

@tnunamak tnunamak commented May 1, 2026

Copy link
Copy Markdown
Member

Summary

Adds the missing companion to POST /v1/grants. Owner can now revoke a grant by id without producing a wallet popup; PS signs GrantRevocation EIP-712 with its delegated serverSigner and submits to the gateway.

Why

Account-app-driven flows (e.g. account.vana.org's account-action revocation UI in PR vana-com/vana-connect#112) need a programmatic revoke path that does not invoke a user wallet popup at revoke time. PS already exposes POST /v1/grants for the create side; this is the symmetric delete side.

Behavior

  • Auth: owner-only (Web3Signed by serverOwner, Bearer access token, or dev token — same model as POST /).
  • Signs GrantRevocation { grantorAddress, grantId } via serverSigner.signGrantRevocation.
  • Calls gateway.revokeGrant({ grantId, grantorAddress, signature }).
  • Returns { status: 'revoked', grantId } on success.
  • 502 on gateway error; 500 if signer is not configured.

Tests

  • happy path verifies signer + gateway calls
  • gateway error returns 502
  • missing serverSigner returns 500

packages/server/src/routes/grants.test.ts — 18 pass / 0 fail.

Scope

Single route added. No middleware changes. No changes to existing routes. No client-side changes (gateway client already exposes revokeGrant).

@tnunamak
tnunamak enabled auto-merge (squash) May 1, 2026 21:28
@github-actions github-actions Bot added the server label May 1, 2026
@github-actions

github-actions Bot commented May 1, 2026

Copy link
Copy Markdown

Codex Review

Findings

  • packages/server/src/routes/grants.ts:251: grantId is only checked for presence, then cast to 0x${string} and signed as EIP-712 bytes32. Malformed path values like /v1/grants/not-a-bytes32 will fail inside signing and return 500 GRANT_REVOCATION_SIGN_FAILED, even though this is client input. Validate as bytes32 first, for example 0x + 64 hex chars or viem isHex(..., { size: 32 }), return 400, and add a test that signer/gateway are not called.

Verification

I attempted to run pnpm test -- packages/server/src/routes/grants.test.ts, but pnpm is not installed. I also tried npm test -- packages/server/src/routes/grants.test.ts, but local dependencies are missing (vitest: not found).

@volod-vana
volod-vana changed the base branch from main to dev July 4, 2026 00:06
Base automatically changed from dev to main July 9, 2026 20:35
@tnunamak
tnunamak force-pushed the tim/account-app-owner-auth branch from 1ffc9c5 to 48723eb Compare August 1, 2026 20:59
volod-vana added a commit that referenced this pull request Aug 27, 2026
…iation (#227)

Personal server slice of durable data point deletion. Sibling PRs
data-gateway #80 (tombstone DELETE) and vana-sdk #195 (deleteDataPoint)
are merged. Rebased onto the write API (#226) and derivative lineage
(#228).

Today a local delete is not durable: the copy goes, sync pulls it back
from the gateway on the next cycle. This PR makes delete stick and
closes that resurrection bug.

`DELETE /v1/data/:scope` (owner auth, same gate as ingest) runs, in this
order:

1. look up the current version on the gateway
2. sign a tombstone AddData for current+1 with the server signer,
exactly as uploads are signed
3. `DELETE /v1/data/:dataPointId` on the gateway; on 409 re-sign once at
the version the gateway names
4. delete blobs from vana-storage by exact key, one `DELETE
/{owner}/{scope}/{version}` per version at or below the tombstone, never
by prefix
5. local `deleteScope`
6. access log entry with `action: "delete"`

The response is a 200 with the per-step result (this replaces the old
204; nothing in unity-surfaces or desktop calls this route). If the
gateway does not acknowledge, the local copy is kept and the route
returns 502. Blob deletion after a gateway success is best effort:
failures leave exact `{scope, version}` markers (range markers for the
registry sequence) that every sync cycle drains at 15 keys per pass,
half of the storage DELETE budget. Nothing ever falls back to a prefix
delete, so a concurrent re-add, which lives above the tombstone version,
survives by construction.

Reads: a `ScopeDeletionTracker` shared by the HTTP API, the MCP read
paths, the paid session port and the sync workers refuses a scope the
gateway reports deleted, before any x402 charge. It is fed by the
includeDeleted feed and by this replica's own deletes, consulted
synchronously, and only falls through to a gateway lookup on a local
miss or a verdict older than 120s. Consistency window is stated in the
code: immediate for this replica's deletes, one poll interval for
another replica's while sync is healthy. PS-Lite wires the same tracker.

Sync reconciliation is causal, not clock based: synced rows are covered
by registry version; unsynced rows are covered unless they carry an
`afterTombstoneVersion` marker stamped at ingest (sqlite schema v4, lite
and memory indexes). Covered entries are dropped and their exact blob
keys queued for cleanup. Delete and sync cycles are serialised through
the sync manager lock; every persisted marker store mutates through a
queue.

The reproduction test is `download.test.ts > deletion reconciliation
(durable delete) > does not resurrect a scope the gateway reports as
deleted`. It fails against main's download worker and passes here.

Scope keys, stated plainly: the scope key is derived (HKDF from the
owner master-key signature) and never stored, on both PS and PS-Lite.
There is nothing to destroy on delete. Durability comes from the
tombstone plus blob removal; ciphertext that left before the delete
stays decryptable by the owner. If that is not acceptable for the health
use case, per-scope stored keys are a separate change.

Tombstone constants are a local copy pinned to the same hex as the SDK
export, with a TODO to switch once a canary ships.

Lineage: `DELETE /v1/data/:scope?cascade=lineage` stays 501. Durable
delete exists now, what is missing is the walk over the gateway graph
(owner view, deepest first, all-or-nothing before the first tombstone,
partial reporting). Single-node delete of a source leaves derivatives in
place; the lineage view reports the source as deleted per node. The
lineage read endpoint is not gated by the deletion tracker on purpose:
it is a signed gateway read and the view already carries `deletedAt`.

Known limits:

- a scope with thousands of versions drains at 15 keys per cycle; faster
needs a bulk endpoint on the storage side
- data ingested on a replica after another replica's delete, before the
first replica learns of it, is treated as covered
- follow-up: advance the incremental `since` watermark on the gateway's
new `changedAt` so revives are picked up without a full re-list

Tests: 1272 unit (1148 on main) plus 26 e2e. Build must precede test, as
in CI.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant